﻿using System;
using System.Linq;

namespace MLSharp
{
	/// <summary>
	/// Stores information about an attribute
	/// in a data set.
	/// </summary>
	public class DataAttribute : ICloneable
	{
		#region Internal Properties

		/// <summary>
		/// Gets or sets the 0-based index of the attribute in the underlying
		/// spreadsheet.
		/// </summary>
		internal int Index { get; set; }

		#endregion

		#region Public Properties

		/// <summary>
		/// Gets or sets the type of the attribute.
		/// </summary>
		public AttributeType Type { get; set; }

		/// <summary>
		/// Gets or sets the name of the attribute
		/// </summary>
		public string Name { get; set; }

		/// <summary>
		/// Gets or sets the number of discrete values the attribute can take on 
		/// (for Discrete attributes only)
		/// </summary>
		public int DiscreteValueCount { get; set; }

		/// <summary>
		/// Gets or sets the set of possible values the attribute can take on
		/// (for Set attributes only)
		/// </summary>
		public string[] PossibleValues { get; set; }

		/// <summary>
		/// Gets or sets the possible values for a discrete attribute.
		/// </summary>
		public double[] DiscreteValues { get; set; }

		#endregion

		#region Public Static Methods

		/// <summary>
		/// Provides a one-line shortcut for creating a new continuous data attribute.
		/// </summary>
		/// <param name="name">The name of the attribute.</param>
		/// <returns>A properly initialized <see cref="DataAttribute"/>.</returns>
		public static DataAttribute NewContinuousAttribute(string name)
		{
			DataAttribute attribute = new DataAttribute();
			attribute.Name = name;
			attribute.Type = AttributeType.Continuous;

			return attribute;
		}

		/// <summary>
		/// Provides a one-line shortcut for creating a new discrete data attribute.
		/// </summary>
		/// <param name="name">The name of the attribute.</param>
		/// <param name="discreteValueCount">The number of possible values this attribute may take.</param>
		/// <returns>A properly initialized <see cref="DataAttribute"/>.</returns>
		public static DataAttribute NewDiscreteAttribute(string name, int discreteValueCount)
		{
			DataAttribute attribute = new DataAttribute();
			attribute.Name = name;
			attribute.Type = AttributeType.Discrete;
			attribute.DiscreteValueCount = discreteValueCount;
			attribute.DiscreteValues = new double[0];

			return attribute;
		}

		/// <summary>
		/// Provides a one-line shortcut for creating a new set data attribute.
		/// </summary>
		/// <param name="name">The name of the attribute.</param>
		/// <param name="possibleValues">The set of possible values.</param>
		/// <returns>A properly initialized <see cref="DataAttribute"/>.</returns>
		public static DataAttribute NewSetAttribute(string name, params string[] possibleValues)
		{
			DataAttribute attribute = new DataAttribute();
			attribute.Name = name;
			attribute.Type = AttributeType.Set;
			attribute.PossibleValues = possibleValues;

			return attribute;
		}

		#endregion

		#region Public Methods

		/// <summary>
		/// Creates a clone.
		/// </summary>
		/// <returns></returns>
		object ICloneable.Clone()
		{
			return Clone();
		}

		/// <summary>
		/// Creates a clone of the instance.
		/// </summary>
		/// <returns></returns>
		public DataAttribute Clone()
		{
			return (DataAttribute)MemberwiseClone();
		}

		/// <summary>
		/// Finds the appropriate classification based on a real-valued output.
		/// </summary>
		/// <param name="value">An output of the neural network.</param>
		/// <returns>The classification.</returns>
		/// <remarks>
		/// This method only works if the possible values can be represented
		/// as floating-point numbers.
		/// </remarks>
		public string GetClosestValue(double value)
		{
			switch (Type)
			{
				//Continuous attributes are easy, just return value.
				case AttributeType.Continuous:
					return value.ToString();
				//For set and discrete attributes, we have to calculate a confidence based on how far it is from the second-closest
				//possible value.
				case AttributeType.Discrete:
				case AttributeType.Set:

					//First, we need to convert the possible values to doubles so we can compare them.
					double[] possibleValues;

					try
					{
						possibleValues = PossibleValues.Select(pv => double.Parse(pv)).ToArray();
					}
					catch (FormatException)
					{
						throw new InvalidOperationException(
							"One or more of the attribute's possible values could not be converted to a double.");
					}

					//Just find the closest match.
					int bestMatch = -1;
					double minDistance = double.MaxValue;

					for (int i = 0; i < possibleValues.Length; i++)
					{
						double distance = Math.Abs(value - possibleValues[i]);

						if (distance < minDistance)
						{
							bestMatch = i;
							minDistance = distance;
						}
					}

					return PossibleValues[bestMatch];

				default:
					throw new NotSupportedException("Attribute type is not supported: " + Type);
			}
		}

		/// <summary>
		/// Computes a confidence value by calculating the distance between the network output 
		/// and the classification result.
		/// </summary>
		/// <param name="value"></param>
		/// <returns></returns>
		/// <remarks>
		/// This method only works if the possible values can be represented
		/// as floating-point numbers.
		/// </remarks>
		public double GetConfidenceForClosestValue(double value)
		{
			switch (Type)
			{
				//We can't compute a confidence for continuous attributes.
				case AttributeType.Continuous:
					return double.NaN;
				//For set and discrete attributes, we have to calculate a confidence based on how far it is from the second-closest
				//possible value.
				case AttributeType.Discrete:
				case AttributeType.Set:

					//Find the two closest possible matches, compute their distance, then determine
					//a normalized confidence for the point based on how close it is.

					double[] possibleValues;

					try
					{
						possibleValues = PossibleValues.Select(pv => double.Parse(pv)).ToArray();
					}
					catch (FormatException)
					{
						throw new InvalidOperationException(
							"One or more of the attribute's possible values could not be converted to a double.");
					}

					int bestMatch = -1, secondBestMatch = -1;
					double minDistance = double.MaxValue, secondMinDistance = double.MaxValue;

					for (int i = 0; i < possibleValues.Length; i++)
					{
						double distance = Math.Abs(value - possibleValues[i]);

						if (distance < minDistance)
						{
							secondBestMatch = bestMatch;
							secondMinDistance = minDistance;
							bestMatch = i;
							minDistance = distance;
						}
						else if (distance < secondMinDistance)
						{
							secondMinDistance = distance;
							secondBestMatch = i;
						}
					}

					return 1.0 - (minDistance / Math.Abs(possibleValues[bestMatch] - possibleValues[secondBestMatch]));

				default:
					throw new NotSupportedException("Attribute type is not supported: " + Type);
			}
		}

		/// <summary>
		/// Returns true if the attribute is nominal-valued (either Set or Discrete).
		/// </summary>
		/// <returns></returns>
		public bool IsNominal()
		{
			return Type == AttributeType.Discrete || Type == AttributeType.Set;
		}

		/// <summary>
		/// Returns true if the attribute is numeric-valued (either Continuous or Discrete).
		/// </summary>
		/// <returns></returns>
		public bool IsNumeric()
		{
			return Type == AttributeType.Continuous || Type == AttributeType.Discrete;
		}

		#endregion
	}
}
